"""批量导出 PSD/PSB 文件的合成图与图层。"""

from __future__ import annotations

import argparse
import logging
import re
import sys
from pathlib import Path
import json
from typing import Iterator, Set

from psd_tools import PSDImage
from collections.abc import Mapping

# 导入 EngineData 解析器
try:
    from psd_tools.psd.engine_data import EngineData as EngineDataParser
except ImportError:
    EngineDataParser = None

INVALID_CHARS_PATTERN = re.compile(r'[<>:"/\\|?*]')


def parse_args(argv: list[str]) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="批量导出 PSD/PSB 文件")
    parser.add_argument(
        "--input",
        type=Path,
        default=Path("psd"),
        help="PSD 文件所在文件夹 (默认: ./psd)",
    )
    parser.add_argument(
        "--output",
        type=Path,
        default=Path("export"),
        help="导出结果输出文件夹 (默认: ./export)",
    )
    parser.add_argument(
        "--include-hidden",
        action="store_true",
        help="同时导出隐藏图层",
    )
    parser.add_argument(
        "--flatten-only",
        action="store_true",
        help="仅导出合成图，不导出单独图层",
    )
    parser.add_argument(
        "--data-only",
        action="store_true",
        help="仅导出图层元数据（不保存图层 PNG）",
    )
    parser.add_argument(
        "--export-json",
        action="store_true",
        help="导出图层与合成图的元数据为 JSON 文件",
    )
    parser.add_argument(
        "--debug-text",
        action="store_true",
        help="为文本图层输出调试信息（仅键摘要，不含像素）",
    )
    parser.add_argument(
        "--log-level",
        default="INFO",
        choices=["DEBUG", "INFO", "WARNING", "ERROR"],
        help="日志级别 (默认: INFO)",
    )
    return parser.parse_args(argv)


def sanitize_name(name: str | None) -> str:
    base = name or "layer"
    base = base.strip() or "layer"
    sanitized = INVALID_CHARS_PATTERN.sub("_", base)
    return sanitized or "layer"


def unique_filename(base: str, used: Set[str]) -> str:
    candidate = base
    index = 1
    while candidate in used:
        candidate = f"{base}_{index}"
        index += 1
    used.add(candidate)
    return candidate


def iter_psd_files(root: Path) -> Iterator[Path]:
    for path in sorted(root.rglob("*.psd")):
        if path.is_file():
            yield path
    for path in sorted(root.rglob("*.psb")):
        if path.is_file():
            yield path


def export_flattened(psd: PSDImage, target_dir: Path, base_name: str) -> Path | None:
    # 使用 force=True 来包含所有图层效果
    try:
        image = psd.composite(force=True)
    except Exception:
        # 如果 force 参数不支持，回退到默认方式
        image = psd.composite()
    
    if image is None:
        logging.warning("文件 %s 合成图为空，跳过。", base_name)
        return None
    output_path = target_dir / f"{base_name}.png"
    image.save(output_path)
    logging.info("合成图已导出到 %s", output_path)
    return output_path


def export_layers(
    psd: PSDImage,
    target_dir: Path,
    include_hidden: bool,
    debug_text: bool = False,
    save_images: bool = True,
) -> list[dict]:
    used_names: Set[str] = set()
    meta: list[dict] = []
    
    for index, layer in enumerate(psd.descendants()):
        if not include_hidden and hasattr(layer, "visible") and not layer.visible:
            logging.debug("跳过隐藏图层 #%d: %s", index, layer.name)
            continue

        # 获取图层图像 - 优先尝试多种方法以包含所有效果
        image = None
        
        # 检测是否为文本图层
        is_text_layer = False
        try:
            from psd_tools.api.layers import TypeLayer
            if isinstance(layer, TypeLayer):
                is_text_layer = True
        except Exception:
            if hasattr(layer, "engine_dict") or hasattr(layer, "text"):
                is_text_layer = True
        
        # 方法1: 对于文本图层，尝试使用 topil() 以获取完整效果
        if is_text_layer:
            try:
                if hasattr(layer, 'topil') and callable(getattr(layer, 'topil', None)):
                    image = layer.topil()
                    logging.debug("图层 #%d 使用 topil() 成功", index)
            except Exception as e:
                logging.debug("图层 #%d topil() 失败: %s", index, e)
        
        # 方法2: 使用 composite(force=True)
        if image is None:
            try:
                image = layer.composite(force=True)
            except Exception:
                try:
                    image = layer.composite()
                except Exception:
                    pass
        
        if image is None:
            logging.debug("图层 #%d (%s) 无像素内容，跳过。", index, layer.name)
            continue

        layer_name = sanitize_name(layer.name)
        if not layer_name:
            layer_name = f"layer_{index:03d}"
        filename = unique_filename(layer_name, used_names)
        output_path = target_dir / f"{filename}.png"
        image_saved = False
        if save_images: image.save(output_path); image_saved = True

        bbox = getattr(layer, "bbox", None)
        bbox_text = f" bbox={tuple(bbox)}" if bbox else ""
        logging.info("导出图层 #%d: %s%s -> %s%s", index, layer.name or "(未命名)", bbox_text, output_path, " (saved)" if image_saved else " (skipped save)")

        bbox_list = None
        width = None
        height = None
        if bbox is not None:
            try:
                # psd-tools BBox supports x1,y1,x2,y2
                bbox_list = [int(bbox.x1), int(bbox.y1), int(bbox.x2), int(bbox.y2)]
                width = int(bbox.width)
                height = int(bbox.height)
            except Exception:  # 容错，避免意外类型导致中断
                try:
                    # 退化为 tuple 转换
                    t = tuple(bbox)
                    if len(t) == 4:
                        bbox_list = [int(t[0]), int(t[1]), int(t[2]), int(t[3])]
                        width = int(t[2]) - int(t[0])
                        height = int(t[3]) - int(t[1])
                except Exception:
                    bbox_list = None

        visible = True
        if hasattr(layer, "visible"):
            try:
                visible = bool(layer.visible)
            except Exception:
                visible = True

        # 检测文本图层并提取样式信息
        is_text_layer = False
        text_meta: dict | None = None
        try:
            # 兼容不同 psd-tools 版本：TypeLayer 或具备 engine_dict/text 属性
            from psd_tools.api.layers import TypeLayer  # type: ignore

            if isinstance(layer, TypeLayer):
                is_text_layer = True
        except Exception:
            # 回退方式
            if hasattr(layer, "engine_dict") or hasattr(layer, "text"):
                is_text_layer = True

        if is_text_layer:
            text_meta = {}
            # 文本内容
            content = ""
            try:
                if getattr(layer, "text", None):
                    content = str(layer.text)
                elif getattr(layer, "text_data", None) and getattr(layer.text_data, "text", None):
                    content = str(layer.text_data.text)
            except Exception:
                content = ""
            text_meta["text"] = content

            def mget(o, k, default=None):
                try:
                    if isinstance(o, Mapping):
                        return o.get(k, default)
                    if hasattr(o, "get"):
                        return o.get(k, default)  # type: ignore[attr-defined]
                    return o[k]  # type: ignore[index]
                except Exception:
                    return default

            def get_chain(o, keys: list[str], default=None):
                cur = o
                for key in keys:
                    cur = mget(cur, key, None)
                    if cur is None:
                        return default
                return cur

            def to_string(val) -> str | None:
                try:
                    if val is None:
                        return None
                    if isinstance(val, bytes):
                        try:
                            return val.decode("utf-8", errors="ignore") or None
                        except Exception:
                            return val.decode("latin1", errors="ignore") or None
                    s = str(val)
                    return s if s else None
                except Exception:
                    return None

            def normalize_font_name(name: str | None) -> str | None:
                if not name:
                    return None
                try:
                    s = str(name).strip()
                    if len(s) >= 2 and ((s[0] == s[-1]) and s[0] in ("'", '"')):
                        s = s[1:-1].strip()
                    return s or None
                except Exception:
                    return name

            def _to_255(v) -> int:
                try:
                    f = float(v)
                    if 0.0 <= f <= 1.0:
                        return max(0, min(255, int(round(f * 255))))
                    return max(0, min(255, int(round(f))))
                except Exception:
                    return 0

            def rgb_to_hex(rgb: list[int] | None) -> str | None:
                if not rgb or len(rgb) != 3:
                    return None
                try:
                    r, g, b = [max(0, min(255, int(x))) for x in rgb]
                    return f"#{r:02X}{g:02X}{b:02X}"
                except Exception:
                    return None

            def effect_color_to_rgb(color) -> list[int] | None:
                try:
                    if color is None:
                        return None
                    
                    # 处理 Descriptor 对象（psd-tools 中常见）
                    # Descriptor 的键可能是字节类型，如 b'Rd  ', b'Grn ', b'Bl  '
                    if hasattr(color, "get") or isinstance(color, Mapping):
                        # 尝试各种可能的键名（字节和字符串）
                        r = None
                        g = None
                        b = None
                        
                        # 字节键（Descriptor 常用）
                        for r_key in [b'Rd  ', b'Red ', b'red ']:
                            r = mget(color, r_key)
                            if r is not None:
                                break
                        for g_key in [b'Grn ', b'Gr  ', b'Green', b'green']:
                            g = mget(color, g_key)
                            if g is not None:
                                break
                        for b_key in [b'Bl  ', b'Blue', b'blue']:
                            b = mget(color, b_key)
                            if b is not None:
                                break
                        
                        # 如果字节键没找到，尝试字符串键
                        if r is None:
                            r = mget(color, "r") or mget(color, "red") or mget(color, "Rd") or mget(color, "Red")
                        if g is None:
                            g = mget(color, "g") or mget(color, "green") or mget(color, "Gr") or mget(color, "Grn") or mget(color, "Green")
                        if b is None:
                            b = mget(color, "b") or mget(color, "blue") or mget(color, "Bl") or mget(color, "Blue")
                        
                        if r is not None and g is not None and b is not None:
                            return [_to_255(r), _to_255(g), _to_255(b)]
                    
                    # 对象：color.r/color.red 等
                    for attrs in (("r", "g", "b"), ("red", "green", "blue")):
                        if all(hasattr(color, a) for a in attrs):
                            return [_to_255(getattr(color, attrs[0])), _to_255(getattr(color, attrs[1])), _to_255(getattr(color, attrs[2]))]
                    
                    # 序列
                    if isinstance(color, (list, tuple)) and len(color) >= 3:
                        return [_to_255(color[0]), _to_255(color[1]), _to_255(color[2])]
                    return None
                except Exception:
                    return None

            def keys_as_strings(val) -> list[str] | None:
                try:
                    from collections.abc import Mapping as _Mapping
                    if not isinstance(val, _Mapping):
                        return None
                    result: list[str] = []
                    for k in val.keys():
                        s = to_string(k)
                        if s is None:
                            try:
                                s = str(k)
                            except Exception:
                                s = None
                        if s is not None:
                            result.append(s)
                    result.sort()
                    return result
                except Exception:
                    return None

            engine_dict = getattr(layer, "engine_dict", None)
            runs: list[dict] = []
            primary_font_name = None
            primary_font_size = None
            primary_stroke = None
            
            # 如果 engine_dict 为 None，尝试从 _record 直接获取
            if engine_dict is None:
                try:
                    _record = getattr(layer, "_record", None)
                    if _record:
                        type_tool_obj = getattr(_record, "type_tool_object_setting", None)
                        if type_tool_obj:
                            # 尝试获取 text_data
                            txt_data = getattr(type_tool_obj, "text_data", None)
                            if txt_data and hasattr(txt_data, "items"):
                                engine_dict = txt_data
                except Exception:
                    pass
            
            # 提前尝试从 effects 获取描边和颜色（作为初始值）
            effects_stroke = None
            effects_fill = None
            try:
                effects = getattr(layer, "effects", None)
                if effects is not None:
                    # 尝试遍历 effects.items - psd-tools 的 effects 是一个特殊对象
                    try:
                        if hasattr(effects, "items"):
                            items = effects.items
                            # items 可能是列表属性，不是方法
                            if callable(items):
                                items = items()
                            if items:
                                for effect in items:
                                    if effect is None:
                                        continue
                                    
                                    # 检查是否是描边效果
                                    effect_type = getattr(effect, "__class__", None)
                                    if effect_type:
                                        type_name = effect_type.__name__ if hasattr(effect_type, "__name__") else str(effect_type)
                                        
                                        # 描边效果（Stroke）
                                        if "stroke" in type_name.lower() or "frame" in type_name.lower():
                                            if getattr(effect, "enabled", True):
                                                s_width = getattr(effect, "size", None) or getattr(effect, "width", None) or getattr(effect, "stroke_width", None)
                                                s_color = effect_color_to_rgb(getattr(effect, "color", None))
                                                if s_width is not None or s_color:
                                                    effects_stroke = {
                                                        "enabled": True,
                                                        "width": float(s_width) if s_width is not None else None,
                                                        "color_rgb": s_color,
                                                        "color_hex": rgb_to_hex(s_color),
                                                    }
                                        
                                        # 颜色叠加（Color Overlay / Solid Fill）
                                        elif "color" in type_name.lower() or "solid" in type_name.lower() or "fill" in type_name.lower():
                                            if getattr(effect, "enabled", True):
                                                effect_color = getattr(effect, "color", None)
                                                effects_fill = effect_color_to_rgb(effect_color)
                    except Exception:
                        pass
                    
                    # 尝试使用 find 方法（psd-tools 的 effects 可能支持）
                    if not effects_stroke and hasattr(effects, "find"):
                        try:
                            # 尝试查找描边效果，常见的类型名称
                            for stroke_type in ["stroke", "Stroke", "frameFX", "FrFX"]:
                                try:
                                    found = effects.find(stroke_type)
                                    if found and getattr(found, "enabled", True):
                                        s_width = getattr(found, "size", None) or getattr(found, "width", None)
                                        s_color = effect_color_to_rgb(getattr(found, "color", None))
                                        if s_width is not None or s_color:
                                            effects_stroke = {
                                                "enabled": True,
                                                "width": float(s_width) if s_width is not None else None,
                                                "color_rgb": s_color,
                                                "color_hex": rgb_to_hex(s_color),
                                            }
                                            break
                                except Exception:
                                    continue
                        except Exception:
                            pass
                    
                    if not effects_fill and hasattr(effects, "find"):
                        try:
                            # 尝试查找颜色叠加效果
                            for fill_type in ["color_overlay", "ColorOverlay", "solidFill", "SoFi"]:
                                try:
                                    found = effects.find(fill_type)
                                    if found and getattr(found, "enabled", True):
                                        effect_color = getattr(found, "color", None)
                                        effects_fill = effect_color_to_rgb(effect_color)
                                        if effects_fill:
                                            break
                                except Exception:
                                    continue
                        except Exception:
                            pass
                    
                    # 回退：尝试旧方式访问
                    if not effects_stroke:
                        try:
                            strokes = getattr(effects, "strokes", None) or getattr(effects, "stroke", None)
                            if strokes:
                                if not isinstance(strokes, list):
                                    strokes = [strokes]
                                for st in strokes:
                                    if getattr(st, "enabled", True):
                                        s_width = getattr(st, "size", None) or getattr(st, "width", None)
                                        s_color = effect_color_to_rgb(getattr(st, "color", None))
                                        if s_width is not None or s_color:
                                            effects_stroke = {
                                                "enabled": True,
                                                "width": float(s_width) if s_width is not None else None,
                                                "color_rgb": s_color,
                                                "color_hex": rgb_to_hex(s_color),
                                            }
                                            break
                        except Exception:
                            pass
                    
                    if not effects_fill:
                        try:
                            solid_fills = getattr(effects, "solid_fills", None) or getattr(effects, "color_overlay", None)
                            if solid_fills:
                                if not isinstance(solid_fills, list):
                                    solid_fills = [solid_fills]
                                for ef in solid_fills:
                                    if getattr(ef, "enabled", True):
                                        effect_color = getattr(ef, "color", None)
                                        effects_fill = effect_color_to_rgb(effect_color)
                                        break
                        except Exception:
                            pass
            except Exception:
                pass

            try:
                # 优先使用 layer.resource_dict，其次回退到 engine_dict 内部
                layer_resource_dict = getattr(layer, "resource_dict", None)
                resource_dict = layer_resource_dict or (
                    get_chain(engine_dict, ["ResourceDict"], {})
                    or get_chain(engine_dict, ["DocumentResources"], {})
                    or {}
                )
                font_set = mget(resource_dict, "FontSet", []) or []

                def resolve_font_info(font_index: int | None) -> dict | None:
                    if font_index is None:
                        return None
                    try:
                        if 0 <= int(font_index) < len(font_set):
                            info = font_set[int(font_index)] or {}
                            # 兼容不同键名
                            name = (
                                to_string(mget(info, "Name"))
                                or to_string(mget(info, "name"))
                                or to_string(mget(info, "PostScriptName"))
                                or to_string(mget(info, "FontName"))
                            )
                            family = to_string(mget(info, "FamilyName") or mget(info, "Family"))
                            style = to_string(mget(info, "StyleName") or mget(info, "Style"))
                            return {"name": name, "family": family, "style": style}
                    except Exception:
                        return None
                    return None

                # 样式运行表位置：常见为 engine_dict["StyleRun"]
                style_run = get_chain(engine_dict, ["StyleRun"], {}) or {}
                default_style = get_chain(style_run, ["DefaultStyle", "StyleSheet", "StyleSheetData"], {}) or {}
                run_array = mget(style_run, "RunArray", []) or []

                def parse_color(color_obj, debug_info=None) -> list[int] | None:
                    try:
                        if color_obj is None:
                            return None
                        # 常见结构：{"Clr": {"Rd":0..1,"Gr":...,"Bl":...}}
                        base = mget(color_obj, "Clr") or mget(color_obj, "SolidColor") or mget(color_obj, "Color") or color_obj
                        
                        # 尝试字节键（Descriptor 常用）
                        r = mget(base, b'Rd  ') or mget(base, "Rd")
                        g = mget(base, b'Grn ') or mget(base, "Gr") or mget(base, "Grn")
                        b = mget(base, b'Bl  ') or mget(base, "Bl")
                        
                        if r is not None and g is not None and b is not None:
                            return [_to_255(r), _to_255(g), _to_255(b)]
                        
                        # 另一种：{"Values": [r,g,b]} 或 {b'Values': [r,g,b]}
                        values = mget(base, b'Values') or mget(base, "Values")
                        if debug_info is not None:
                            debug_info["values_obtained"] = str(values)[:50] if values else None
                            debug_info["values_type"] = str(type(values).__name__) if values else None
                            debug_info["values_is_list_tuple"] = isinstance(values, (list, tuple)) if values else False
                            debug_info["values_len"] = len(values) if values and hasattr(values, "__len__") else None
                        
                        # 支持任何类列表对象（鸭子类型：有 __len__ 和 __getitem__ 就行）
                        if values is not None and hasattr(values, "__len__") and hasattr(values, "__getitem__"):
                            try:
                                if len(values) >= 3:
                                    result = [_to_255(values[0]), _to_255(values[1]), _to_255(values[2])]
                                    if debug_info is not None:
                                        debug_info["conversion_success"] = True
                                    return result
                            except (IndexError, TypeError):
                                pass
                        
                        # 直接检查对象的属性（如果是 psd-tools 的对象）
                        if hasattr(base, "values") and callable(getattr(base, "values", None)):
                            # values 是方法，不是属性，跳过
                            pass
                        elif hasattr(base, "values") and isinstance(base.values, (list, tuple)) and len(base.values) >= 3:
                            return [_to_255(base.values[0]), _to_255(base.values[1]), _to_255(base.values[2])]
                        
                        return None
                    except Exception as e:
                        if debug_info is not None:
                            debug_info["parse_exception"] = str(e)
                        return None

                def parse_stroke(stroke_style_obj) -> dict | None:
                    try:
                        if stroke_style_obj is None:
                            return None
                        enabled = mget(stroke_style_obj, "StrokeStyleEnabled")
                        width_val = (
                            mget(stroke_style_obj, "StrokeStyleLineWidth")
                            or mget(stroke_style_obj, "LineWidth")
                            or mget(stroke_style_obj, "StrokeStyleLineWidthFloat")
                        )
                        content_obj = mget(stroke_style_obj, "StrokeStyleContent") or {}
                        color_rgb = parse_color(content_obj)
                        try:
                            width_num = float(width_val) if width_val is not None else None
                        except Exception:
                            width_num = None
                        return {
                            "enabled": bool(enabled) if enabled is not None else True,
                            "width": width_num,
                            "color_rgb": color_rgb,
                            "color_hex": rgb_to_hex(color_rgb),
                        }
                    except Exception:
                        return None

                for run in run_array:
                    sheet = get_chain(run, ["StyleSheet", "StyleSheetData"], {}) or {}
                    font_index = mget(sheet, "Font")
                    if font_index is None:
                        font_index = mget(default_style, "Font")
                    font_info = resolve_font_info(font_index)
                    # 回退：有些文档直接在样式里放字符串型 FontName
                    font_name_str = (
                        to_string(mget(sheet, "FontName"))
                        or to_string(mget(default_style, "FontName"))
                        or to_string(mget(sheet, "PostScriptName"))
                        or to_string(mget(default_style, "PostScriptName"))
                        or to_string(mget(sheet, "Name"))
                        or to_string(mget(default_style, "Name"))
                    )
                    font_name = (font_info.get("name") if font_info else None) or font_name_str
                    font_name = normalize_font_name(font_name)
                    font_size = mget(sheet, "FontSize") or mget(default_style, "FontSize")
                    stroke = parse_stroke(mget(sheet, "StrokeStyle") or mget(default_style, "StrokeStyle"))
                    # 字体填充颜色
                    fill_color_obj = (
                        mget(sheet, "FillColor")
                        or mget(default_style, "FillColor")
                        or mget(sheet, "TextFillColor")
                        or mget(default_style, "TextFillColor")
                        or mget(sheet, "Color")
                        or mget(default_style, "Color")
                    )
                    fill_rgb = parse_color(fill_color_obj)
                    try:
                        size_num = float(font_size) if font_size is not None else None
                    except Exception:
                        size_num = None
                    run_meta = {
                        "font_name": font_name,
                        "font_family": (font_info.get("family") if font_info else None),
                        "font_style": (font_info.get("style") if font_info else None),
                        "font_size": size_num,
                        "stroke": stroke,
                        "fill_color_rgb": fill_rgb,
                        "fill_color_hex": rgb_to_hex(fill_rgb),
                    }
                    runs.append(run_meta)

                if runs:
                    primary_font_name = normalize_font_name(runs[0].get("font_name"))
                    primary_font_size = runs[0].get("font_size")
                    primary_stroke = runs[0].get("stroke")
                    primary_fill_rgb = runs[0].get("fill_color_rgb")
                else:
                    primary_fill_rgb = None
                
                # 如果没有从样式运行中获取到，使用 effects 的值
                if not primary_stroke and effects_stroke:
                    primary_stroke = effects_stroke
                if not primary_fill_rgb and effects_fill:
                    primary_fill_rgb = effects_fill

                # 图层样式（效果）回退：颜色叠加 / 描边
                try:
                    effects = getattr(layer, "effects", None)
                    if effects is not None:
                        # 颜色叠加
                        effect_fill = None
                        try:
                            solid_fills = getattr(effects, "solid_fills", None)
                            if solid_fills:
                                # 取第一个启用的
                                for ef in solid_fills:
                                    if getattr(ef, "enabled", True):
                                        effect_fill = ef
                                        break
                        except Exception:
                            effect_fill = None
                        if effect_fill is not None and not primary_fill_rgb:
                            effect_color = getattr(effect_fill, "color", None)
                            primary_fill_rgb = effect_color_to_rgb(effect_color) or primary_fill_rgb

                        # 描边
                        if primary_stroke is None or not primary_stroke.get("color_rgb"):
                            effect_stroke = None
                            try:
                                strokes = getattr(effects, "strokes", None)
                                if strokes:
                                    for st in strokes:
                                        if getattr(st, "enabled", True):
                                            effect_stroke = st
                                            break
                            except Exception:
                                effect_stroke = None
                            if effect_stroke is not None:
                                s_width = getattr(effect_stroke, "size", None) or getattr(effect_stroke, "width", None)
                                s_color = effect_color_to_rgb(getattr(effect_stroke, "color", None))
                                primary_stroke = {
                                    "enabled": True,
                                    "width": float(s_width) if s_width is not None else None,
                                    "color_rgb": s_color,
                                    "color_hex": rgb_to_hex(s_color),
                                }
                except Exception:
                    pass
                
                # 补充：尝试从 tagged_blocks 获取描边信息
                try:
                    if primary_stroke is None or not primary_stroke.get("color_rgb"):
                        tagged_blocks = getattr(layer, "tagged_blocks", None)
                        if tagged_blocks:
                            # 查找描边效果块 (vstk)
                            vstk = tagged_blocks.get_data(b'vstk')
                            if vstk is not None:
                                try:
                                    enabled = mget(vstk, "strokeEnabled") or mget(vstk, "strokeStyleEnabled")
                                    if enabled:
                                        stroke_width = mget(vstk, "strokeStyleLineWidth") or mget(vstk, "lineWidth")
                                        content = mget(vstk, "strokeStyleContent") or mget(vstk, "content")
                                        if content:
                                            stroke_color = parse_color(content)
                                            if stroke_color or stroke_width:
                                                primary_stroke = {
                                                    "enabled": bool(enabled),
                                                    "width": float(stroke_width) if stroke_width is not None else None,
                                                    "color_rgb": stroke_color,
                                                    "color_hex": rgb_to_hex(stroke_color),
                                                }
                                except Exception:
                                    pass
                except Exception:
                    pass
            except Exception:
                pass

            # 补充回退：psd_tools 的 text_data.fontset
            try:
                if not primary_font_name:
                    td = getattr(layer, "text_data", None)
                    fontset = getattr(td, "fontset", None)
                    if fontset and len(fontset) > 0:
                        f0 = fontset[0]
                        # 兼容对象或字典
                        name = getattr(f0, "name", None) if hasattr(f0, "name") else mget(f0, "name")
                        family = getattr(f0, "family", None) if hasattr(f0, "family") else mget(f0, "family")
                        style = getattr(f0, "style", None) if hasattr(f0, "style") else mget(f0, "style")
                        primary_font_name = normalize_font_name(to_string(name)) or primary_font_name
                        if not runs:
                            runs.append({
                                "font_name": to_string(name),
                                "font_family": to_string(family),
                                "font_style": to_string(style),
                                "font_size": primary_font_size,
                                "stroke": primary_stroke,
                            })
            except Exception:
                pass

            # 再次回退：如果 ResourceDict.FontSet 仅有一个条目，则采用它
            try:
                if not primary_font_name:
                    resource_dict = layer_resource_dict or (
                        get_chain(engine_dict, ["ResourceDict"], {})
                        or get_chain(engine_dict, ["DocumentResources"], {})
                        or {}
                    )
                    font_set = mget(resource_dict, "FontSet", []) or []
                    if isinstance(font_set, list) and len(font_set) == 1:
                        info = font_set[0] or {}
                        name = (
                            to_string(mget(info, "Name"))
                            or to_string(mget(info, "name"))
                            or to_string(mget(info, "PostScriptName"))
                            or to_string(mget(info, "FontName"))
                        )
                        if name:
                            primary_font_name = normalize_font_name(name)
                            if not runs:
                                runs.append({
                                    "font_name": normalize_font_name(name),
                                    "font_family": to_string(mget(info, "FamilyName") or mget(info, "Family")),
                                    "font_style": to_string(mget(info, "StyleName") or mget(info, "Style")),
                                    "font_size": primary_font_size,
                                    "stroke": primary_stroke,
                                })
            except Exception:
                pass

                # 最终回退：直接从 _record 的 EngineData 中提取
                try:
                    if not primary_fill_rgb or (not primary_stroke or not primary_stroke.get("color_rgb")):
                        _record = getattr(layer, "_record", None)
                        if _record:
                            # 优先尝试从 tagged_blocks 获取
                            text_block = None
                            tagged_blocks = getattr(_record, "tagged_blocks", None)
                            if tagged_blocks:
                                try:
                                    tysh_data = tagged_blocks.get_data(b'TySh')
                                    if tysh_data:
                                        text_block = tysh_data
                                        # 尝试从 TySh 的 text_data 中读取 EngineData
                                        tysh_text_data = getattr(text_block, "text_data", None)
                                        if tysh_text_data and hasattr(tysh_text_data, "get"):
                                            # 尝试读取 b'EngineData'
                                            engine_data_raw = tysh_text_data.get(b'EngineData')
                                            if engine_data_raw:
                                                ed = None
                                                # EngineData 可能是 RawData 对象，需要解析
                                                # RawData 使用 .value 属性获取数据（可能已经解析好）
                                                if hasattr(engine_data_raw, "value"):
                                                    # RawData 对象，获取值
                                                    try:
                                                        raw_value = engine_data_raw.value
                                                        # 检查是否已经是字典（psd-tools 可能已经解析好了）
                                                        if isinstance(raw_value, dict) or hasattr(raw_value, "items"):
                                                            ed = raw_value
                                                        # 如果是原始字节，尝试解析
                                                        elif isinstance(raw_value, bytes) and EngineDataParser:
                                                            if len(raw_value) >= 10:
                                                                parsed = EngineDataParser.frombytes(raw_value)
                                                                if parsed and hasattr(parsed, "items"):
                                                                    ed = parsed
                                                    except Exception:
                                                        pass
                                                # 如果已经是字典或类似字典的对象
                                                elif isinstance(engine_data_raw, dict) or hasattr(engine_data_raw, "items"):
                                                    ed = engine_data_raw
                                                
                                                if ed:
                                                    # 尝试获取颜色
                                                    if not primary_fill_rgb:
                                                        # 尝试从 EngineDict -> StyleRun 获取（更常见的路径）
                                                        engine_dict = mget(ed, "EngineDict")
                                                        if engine_dict:
                                                            style_run_data = mget(engine_dict, "StyleRun")
                                                            if style_run_data:
                                                                run_array = mget(style_run_data, "RunArray")
                                                                if run_array and len(run_array) > 0:
                                                                    first_run = run_array[0]
                                                                    style_sheet = get_chain(first_run, ["StyleSheet", "StyleSheetData"], {})
                                                                    if style_sheet:
                                                                        # 尝试获取 FillColor
                                                                        fill_color = mget(style_sheet, "FillColor")
                                                                        if fill_color:
                                                                            primary_fill_rgb = parse_color(fill_color)
                                                        
                                                        # 如果还是没有，尝试其他路径
                                                        if not primary_fill_rgb:
                                                            raw_color = (
                                                                get_chain(ed, ["EngineDict", "Editor", "Text"]) 
                                                                or get_chain(ed, ["DocumentResources", "Color"])
                                                            )
                                                            if raw_color:
                                                                primary_fill_rgb = parse_color(raw_color) or primary_fill_rgb
                                                    
                                                    # 尝试获取描边
                                                    if not primary_stroke or not primary_stroke.get("color_rgb"):
                                                        raw_stroke = get_chain(ed, ["EngineDict", "StyleRun", "RunArray", 0, "StyleSheet", "StyleSheetData", "StrokeStyle"]) 
                                                        if raw_stroke:
                                                            primary_stroke = parse_stroke(raw_stroke) or primary_stroke
                                except Exception:
                                    pass
                            
                            # 如果没有找到，尝试旧方式
                            if not text_block or not primary_fill_rgb:
                                text_block_old = getattr(_record, "text_data", None) or getattr(_record, "type_tool_object_setting", None)
                                if text_block_old:
                                    # 尝试直接获取 EngineData
                                    raw_engine = getattr(text_block_old, "text_data", None) or getattr(text_block_old, "engine_data", None)
                                    if raw_engine and hasattr(raw_engine, "items"):
                                        # 可能是原始字典
                                        ed = raw_engine
                                        # 尝试获取默认颜色
                                        if not primary_fill_rgb:
                                            raw_color = get_chain(ed, ["EngineDict", "Editor", "Text"]) or get_chain(ed, ["DocumentResources", "Color"])
                                            primary_fill_rgb = parse_color(raw_color) or primary_fill_rgb
                                            
                                            # 如果还是没有，尝试从 StyleRun 获取
                                            if not primary_fill_rgb:
                                                style_run_data = get_chain(ed, ["StyleRun"], None)
                                                if style_run_data:
                                                    run_array = mget(style_run_data, "RunArray", [])
                                                    if run_array and len(run_array) > 0:
                                                        first_run = run_array[0]
                                                        style_sheet = get_chain(first_run, ["StyleSheet", "StyleSheetData"], {})
                                                        if style_sheet:
                                                            # 尝试获取 FillColor
                                                            fill_color = mget(style_sheet, "FillColor")
                                                            if fill_color:
                                                                primary_fill_rgb = parse_color(fill_color)
                                        
                                        # 尝试获取描边
                                        if not primary_stroke or not primary_stroke.get("color_rgb"):
                                            raw_stroke = get_chain(ed, ["EngineDict", "StyleRun", "RunArray", 0, "StyleSheet", "StyleSheetData", "StrokeStyle"]) 
                                            if raw_stroke:
                                                primary_stroke = parse_stroke(raw_stroke) or primary_stroke
                except Exception:
                    pass

            text_meta["font_name"] = primary_font_name
            text_meta["font_size"] = primary_font_size
            text_meta["stroke"] = primary_stroke
            text_meta["runs"] = runs
            text_meta["fill_color_rgb"] = primary_fill_rgb
            text_meta["fill_color_hex"] = rgb_to_hex(primary_fill_rgb)

        layer_meta = {
            "index": index,
            "original_name": layer.name or "",
            "sanitized_name": filename,
            "filename": f"{filename}.png",
            "image_saved": image_saved,
            "bbox": bbox_list,
            "width": width,
            "height": height,
            "visible": visible,
        }

        if is_text_layer:
            layer_meta["is_text_layer"] = True
            if text_meta is not None:
                layer_meta["text"] = text_meta.get("text")
                layer_meta["font_name"] = text_meta.get("font_name")
                layer_meta["font_size"] = text_meta.get("font_size")
                layer_meta["stroke"] = text_meta.get("stroke")
                layer_meta["text_runs"] = text_meta.get("runs", [])
                layer_meta["font_color_rgb"] = text_meta.get("fill_color_rgb")
                layer_meta["font_color_hex"] = text_meta.get("fill_color_hex")

            # 可选调试信息（已禁用以保持输出简洁）
            if False and debug_text:
                try:
                    style_run = get_chain(engine_dict, ["StyleRun"], {}) or {}
                    default_style = get_chain(style_run, ["DefaultStyle", "StyleSheet", "StyleSheetData"], {}) or {}
                    run_array = mget(style_run, "RunArray", []) or []
                    resource_dict = (
                        get_chain(engine_dict, ["ResourceDict"], {})
                        or get_chain(engine_dict, ["DocumentResources"], {})
                        or {}
                    )
                    font_set = mget(resource_dict, "FontSet", []) or []
                    font_names_preview = []
                    for fi in font_set[:5]:
                        n = to_string(mget(fi, "Name") or mget(fi, "PostScriptName") or mget(fi, "FontName"))
                        if n:
                            font_names_preview.append(n)
                    first_run_sheet = get_chain(run_array[0], ["StyleSheet", "StyleSheetData"], {}) if run_array else None
                    
                    # 增强调试：输出 engine_dict 的顶层键
                    engine_dict_keys = keys_as_strings(engine_dict) if engine_dict else None
                    style_run_keys = keys_as_strings(style_run) if style_run else None
                    
                    # 检查 effects 信息
                    effects_debug = None
                    try:
                        effects = getattr(layer, "effects", None)
                        if effects:
                            effects_list = []
                            try:
                                if hasattr(effects, "items"):
                                    items = effects.items
                                    # items 可能是列表属性，不是方法
                                    if callable(items):
                                        items = items()
                                    if items:
                                        for effect in items:
                                            if effect is not None:
                                                effect_type = getattr(effect, "__class__", None)
                                                type_name = effect_type.__name__ if effect_type and hasattr(effect_type, "__name__") else str(type(effect))
                                                effect_enabled = getattr(effect, "enabled", None)
                                                
                                                # 如果是 Stroke，提取 color 详细信息
                                                color_debug = None
                                                if "stroke" in type_name.lower():
                                                    try:
                                                        color_obj = getattr(effect, "color", None)
                                                        if color_obj:
                                                            color_debug = {
                                                                "color_type": str(type(color_obj).__name__),
                                                                "color_attrs": [x for x in dir(color_obj) if not x.startswith('_')][:20],
                                                            }
                                                            # 尝试读取颜色值（包括字节键）
                                                            if hasattr(color_obj, "get"):
                                                                for key in [b'Rd  ', b'Grn ', b'Bl  ', 'Rd', 'Grn', 'Bl', 'r', 'g', 'b']:
                                                                    val = color_obj.get(key)
                                                                    if val is not None:
                                                                        color_debug[str(key)] = str(val)[:50]
                                                            
                                                            # 尝试通过函数提取完整 RGB
                                                            rgb_extracted = effect_color_to_rgb(color_obj)
                                                            if rgb_extracted:
                                                                color_debug["rgb_extracted"] = rgb_extracted
                                                    except Exception as e:
                                                        color_debug = {"error": str(e)}
                                                
                                                effect_info = {
                                                    "type": type_name,
                                                    "enabled": effect_enabled,
                                                    "attrs": [x for x in dir(effect) if not x.startswith('_')][:20],  # 限制数量
                                                }
                                                if color_debug:
                                                    effect_info["color_debug"] = color_debug
                                                effects_list.append(effect_info)
                            except Exception as e:
                                effects_list = [{"error": str(e)}]
                            
                            effects_debug = {
                                "has_effects": True,
                                "has_strokes": bool(getattr(effects, "strokes", None)),
                                "has_solid_fills": bool(getattr(effects, "solid_fills", None)),
                                "effects_attrs": [x for x in dir(effects) if not x.startswith('_')],
                                "effects_items": effects_list,
                            }
                    except Exception as e:
                        effects_debug = {"error": f"Failed to read effects: {e}"}
                    
                    # 尝试从 _record 获取更多调试信息
                    record_debug = {"status": "checking"}
                    try:
                        _record = getattr(layer, "_record", None)
                        if _record:
                            record_debug["has_record"] = True
                            record_debug["record_attrs"] = [x for x in dir(_record) if not x.startswith('_')][:30]
                            
                            # 尝试从 tagged_blocks 获取文本数据
                            tagged_blocks = getattr(_record, "tagged_blocks", None)
                            if tagged_blocks:
                                record_debug["has_tagged_blocks"] = True
                                # 尝试获取 TySh (Type Tool Object Setting)
                                try:
                                    tysh_data = tagged_blocks.get_data(b'TySh')
                                    if tysh_data:
                                        record_debug["has_TySh"] = True
                                        # TySh 包含 text_data
                                        txt_data = getattr(tysh_data, "text_data", None)
                                        if txt_data:
                                            record_debug["TySh_has_text_data"] = True
                                            record_debug["TySh_text_data_type"] = str(type(txt_data).__name__)
                                            record_debug["TySh_text_data_attrs"] = [x for x in dir(txt_data) if not x.startswith('_')][:30]
                                            
                                            # 尝试多种方式获取键
                                            try:
                                                if hasattr(txt_data, "keys"):
                                                    keys_list = list(txt_data.keys())
                                                    record_debug["TySh_text_data_keys_raw"] = str(keys_list)[:200]
                                                    record_debug["TySh_text_data_keys"] = keys_as_strings(txt_data)
                                            except Exception as e:
                                                record_debug["TySh_keys_error"] = str(e)
                                            
                                            # 尝试读取一些可能的键的值
                                            if hasattr(txt_data, "get"):
                                                record_debug["TySh_sample_values"] = {}
                                                for key in [b'Txt ', 'Txt', b'EngineData', 'EngineData', b'EngineDict', 'EngineDict', 
                                                           b'StyleRun', 'StyleRun', b'RunArray', 'RunArray', b'FontSet', 'FontSet']:
                                                    try:
                                                        val = txt_data.get(key)
                                                        if val is not None:
                                                            record_debug["TySh_sample_values"][str(key)] = str(type(val).__name__)
                                                    except Exception:
                                                        pass
                                                
                                                # 特别检查 EngineData
                                                try:
                                                    engine_data_raw = txt_data.get(b'EngineData')
                                                    if engine_data_raw:
                                                        record_debug["has_EngineData"] = True
                                                        record_debug["EngineData_type"] = str(type(engine_data_raw).__name__)
                                                        record_debug["EngineData_has_data_attr"] = hasattr(engine_data_raw, "data")
                                                        record_debug["EngineData_attrs"] = [x for x in dir(engine_data_raw) if not x.startswith('_')][:30]
                                                        
                                                        # 尝试解析 RawData (使用 .value 属性)
                                                        if hasattr(engine_data_raw, "value"):
                                                            try:
                                                                raw_value = engine_data_raw.value
                                                                record_debug["EngineData_value_type"] = str(type(raw_value).__name__)
                                                                
                                                                # 检查是否已经是字典
                                                                if isinstance(raw_value, dict) or hasattr(raw_value, "items"):
                                                                    record_debug["EngineData_already_dict"] = True
                                                                    if hasattr(raw_value, "keys"):
                                                                        eng_keys = list(raw_value.keys())
                                                                        record_debug["EngineData_dict_keys"] = str(eng_keys)[:200]
                                                                    
                                                                    # 尝试追踪颜色提取路径
                                                                    try:
                                                                        engine_dict = mget(raw_value, "EngineDict")
                                                                        record_debug["has_EngineDict"] = bool(engine_dict)
                                                                        if engine_dict:
                                                                            style_run = mget(engine_dict, "StyleRun")
                                                                            record_debug["has_StyleRun"] = bool(style_run)
                                                                            if style_run:
                                                                                run_array = mget(style_run, "RunArray")
                                                                                record_debug["has_RunArray"] = bool(run_array)
                                                                                record_debug["RunArray_len"] = len(run_array) if run_array else 0
                                                                                if run_array and len(run_array) > 0:
                                                                                    first_run = run_array[0]
                                                                                    record_debug["first_run_type"] = str(type(first_run).__name__)
                                                                                    style_sheet = get_chain(first_run, ["StyleSheet", "StyleSheetData"], {})
                                                                                    record_debug["has_StyleSheetData"] = bool(style_sheet)
                                                                                    if style_sheet:
                                                                                        fill_color = mget(style_sheet, "FillColor")
                                                                                        record_debug["has_FillColor"] = bool(fill_color)
                                                                                        if fill_color:
                                                                                            record_debug["FillColor_preview"] = str(fill_color)[:100]
                                                                                            record_debug["FillColor_type"] = str(type(fill_color).__name__)
                                                                                            
                                                                                            # 检查如何访问 Values
                                                                                            try:
                                                                                                if hasattr(fill_color, "keys"):
                                                                                                    keys_list = list(fill_color.keys())
                                                                                                    record_debug["FillColor_keys"] = str(keys_list)[:100]
                                                                                                
                                                                                                # 尝试多种方式获取 Values
                                                                                                values_str = mget(fill_color, "Values")
                                                                                                values_bytes = mget(fill_color, b'Values')
                                                                                                values_attr = getattr(fill_color, "values", None) if hasattr(fill_color, "values") else None
                                                                                                
                                                                                                record_debug["Values_str"] = str(values_str)[:50] if values_str else None
                                                                                                record_debug["Values_bytes"] = str(values_bytes)[:50] if values_bytes else None
                                                                                                record_debug["Values_attr"] = str(values_attr)[:50] if values_attr else None
                                                                                            except Exception as e:
                                                                                                record_debug["FillColor_access_error"] = str(e)
                                                                                            
                                                                                            # 尝试解析颜色
                                                                                            try:
                                                                                                parse_debug = {}
                                                                                                parsed_color = parse_color(fill_color, parse_debug)
                                                                                                record_debug["FillColor_parsed_result"] = str(parsed_color)
                                                                                                record_debug["FillColor_parse_debug"] = parse_debug
                                                                                            except Exception as e:
                                                                                                record_debug["FillColor_parse_error"] = str(e)
                                                                    except Exception as e:
                                                                        record_debug["color_trace_error"] = str(e)
                                                                # 如果是字节
                                                                elif isinstance(raw_value, bytes):
                                                                    record_debug["EngineData_is_bytes"] = True
                                                                    record_debug["EngineData_bytes_len"] = len(raw_value)
                                                                    record_debug["EngineData_parser_available"] = bool(EngineDataParser)
                                                                    
                                                                    # 尝试使用解析器
                                                                    if EngineDataParser and len(raw_value) >= 10:
                                                                        try:
                                                                            parsed = EngineDataParser.frombytes(raw_value)
                                                                            if parsed:
                                                                                record_debug["EngineData_parsed"] = True
                                                                                if hasattr(parsed, "keys"):
                                                                                    eng_keys = list(parsed.keys())
                                                                                    record_debug["EngineData_parsed_keys"] = str(eng_keys)[:200]
                                                                        except Exception as parse_err:
                                                                            record_debug["EngineData_parse_error"] = str(parse_err)
                                                                    elif len(raw_value) < 10:
                                                                        record_debug["EngineData_too_short"] = True
                                                            except Exception as e:
                                                                record_debug["EngineData_value_error"] = str(e)
                                                        
                                                        # 如果是字典或类似字典的对象
                                                        elif isinstance(engine_data_raw, dict) or hasattr(engine_data_raw, "items"):
                                                            if hasattr(engine_data_raw, "keys"):
                                                                eng_keys = list(engine_data_raw.keys())
                                                                record_debug["EngineData_keys"] = str(eng_keys)[:200]
                                                        # 如果是字符串（原始 EngineData）
                                                        elif isinstance(engine_data_raw, (str, bytes)):
                                                            record_debug["EngineData_preview"] = str(engine_data_raw)[:100]
                                                except Exception as e:
                                                    record_debug["EngineData_error"] = str(e)
                                except Exception as e:
                                    record_debug["TySh_error"] = str(e)
                            
                            type_obj = getattr(_record, "type_tool_object_setting", None)
                            if type_obj:
                                record_debug["has_type_obj"] = True
                                record_debug["type_obj_attrs"] = [x for x in dir(type_obj) if not x.startswith('_')][:30]
                                
                                # 查看 text_data 的结构
                                txt_data = getattr(type_obj, "text_data", None)
                                if txt_data:
                                    record_debug["has_text_data"] = True
                                    record_debug["text_data_type"] = str(type(txt_data).__name__)
                                    record_debug["text_data_keys"] = keys_as_strings(txt_data) if hasattr(txt_data, "keys") else None
                                    # 尝试获取样式信息
                                    if hasattr(txt_data, "get"):
                                        record_debug["text_data_has_get"] = True
                                        for key in [b'StyleRun', 'StyleRun', b'EngineDict', 'EngineDict']:
                                            val = txt_data.get(key)
                                            if val:
                                                record_debug[f"found_{str(key)}"] = True
                                                if hasattr(val, "keys"):
                                                    record_debug[f"{str(key)}_keys"] = keys_as_strings(val)
                                else:
                                    record_debug["has_text_data"] = False
                            else:
                                record_debug["has_type_obj"] = False
                        else:
                            record_debug["has_record"] = False
                    except Exception as e:
                        record_debug["error"] = str(e)
                    
                    layer_meta["debug_text"] = {
                        "layer_kind": getattr(layer, "kind", None),
                        "has_resource_dict": bool(layer_resource_dict is not None),
                        "engine_dict_keys": engine_dict_keys,
                        "style_run_keys": style_run_keys,
                        "default_style_keys": keys_as_strings(default_style),
                        "run_count": len(run_array),
                        "first_run_keys": keys_as_strings(first_run_sheet),
                        "fontset_preview": font_names_preview,
                        "fontset_len": len(font_set) if isinstance(font_set, list) else None,
                        "effects": effects_debug,
                        "effects_stroke_found": effects_stroke is not None,
                        "effects_fill_found": effects_fill is not None,
                        "record_debug": record_debug,
                    }

                    # 日志输出每个样式段（与用户示例一致）
                    run_lengths = mget(style_run, "RunLengthArray", []) or []
                    text_content = text_meta.get("text") or ""
                    cursor = 0
                    for i, (length, style) in enumerate(zip(run_lengths, run_array)):
                        try:
                            sheet = get_chain(style, ["StyleSheet", "StyleSheetData"], {}) or {}
                            font_index = mget(sheet, "Font")
                            # 优先通过 FontSet 解析
                            font_name_log = None
                            try:
                                if font_index is not None and 0 <= int(font_index) < len(font_set):
                                    fobj = font_set[int(font_index)] or {}
                                    font_name_log = (
                                        to_string(mget(fobj, "Name"))
                                        or to_string(mget(fobj, "PostScriptName"))
                                        or to_string(mget(fobj, "FontName"))
                                    )
                            except Exception:
                                pass
                            if not font_name_log:
                                font_name_log = (
                                    to_string(mget(sheet, "FontName"))
                                    or to_string(mget(sheet, "PostScriptName"))
                                    or to_string(mget(sheet, "Name"))
                                )
                            font_size_log = mget(sheet, "FontSize")
                            stroke_raw = mget(sheet, "StrokeStyle")
                            snippet = ""
                            try:
                                seg_len = int(length) if length is not None else 0
                                snippet = text_content[cursor:cursor + max(0, seg_len)]
                                cursor += max(0, seg_len)
                            except Exception:
                                snippet = ""
                            stroke_keys = list(stroke_raw.keys()) if isinstance(stroke_raw, Mapping) else None
                            logging.info(
                                "TEXT[%s] run#%d: '%s' font=%s size=%s stroke_keys=%s sheet_keys=%s",
                                layer.name or "",
                                i,
                                snippet,
                                font_name_log,
                                font_size_log,
                                stroke_keys,
                                keys_as_strings(sheet),
                            )
                        except Exception as _exc:
                            logging.debug("解析文字样式段失败: %s", _exc)
                except Exception:
                    pass

        meta.append(layer_meta)

    return meta


def ensure_output_dir(path: Path) -> None:
    path.mkdir(parents=True, exist_ok=True)


def main(argv: list[str]) -> int:
    args = parse_args(argv)
    logging.basicConfig(level=getattr(logging, args.log_level))

    if not args.input.exists():
        logging.error("输入目录不存在: %s", args.input)
        return 1

    ensure_output_dir(args.output)

    psd_files = list(iter_psd_files(args.input))
    if not psd_files:
        logging.warning("在 %s 中未找到 PSD/PSB 文件。", args.input)
        return 0

    for psd_path in psd_files:
        logging.info("处理文件: %s", psd_path)
        try:
            psd = PSDImage.open(psd_path)
        except Exception as exc:  # pylint: disable=broad-except
            logging.error("打开文件失败 %s: %s", psd_path, exc)
            continue

        project_dir = args.output / psd_path.stem
        ensure_output_dir(project_dir)

        flattened_path = export_flattened(psd, project_dir, psd_path.stem)
        layers_meta: list[dict] = []
        if not args.flatten_only:
            layers_meta = export_layers(
                psd,
                project_dir,
                args.include_hidden,
                args.debug_text,
                save_images=(not args.data_only),
            )
        else:
            # flatten-only：若需要 JSON/调试，则也遍历图层但不保存图片
            if args.export_json or args.debug_text:
                layers_meta = export_layers(
                    psd,
                    project_dir,
                    args.include_hidden,
                    args.debug_text,
                    save_images=False,
                )

        if args.export_json:
            meta_doc = {
                "psd_file": psd_path.name,
                "canvas_width": getattr(psd, "width", None),
                "canvas_height": getattr(psd, "height", None),
                "flattened_png": flattened_path.name if flattened_path else None,
                "layers": layers_meta,
            }
            meta_path = project_dir / "layers.json"
            try:
                # 递归清洗，确保全部可序列化
                def sanitize_for_json(value):
                    try:
                        from collections.abc import Mapping as _Mapping, Iterable as _Iterable
                        import math
                        # 基础类型直接返回
                        if value is None or isinstance(value, (str, int, float, bool)):
                            # 处理非数值的 NaN/Inf
                            if isinstance(value, float) and (math.isnan(value) or math.isinf(value)):
                                return None
                            return value
                        # Path
                        if isinstance(value, Path):
                            return str(value)
                        # bytes
                        if isinstance(value, (bytes, bytearray)):
                            try:
                                return value.decode("utf-8", errors="ignore")
                            except Exception:
                                return value.decode("latin1", errors="ignore")
                        # 映射/字典
                        if isinstance(value, _Mapping):
                            out = {}
                            for k, v in value.items():
                                try:
                                    key_str = str(k)
                                except Exception:
                                    key_str = repr(k)
                                out[key_str] = sanitize_for_json(v)
                            return out
                        # 可迭代（但排除字符串本身）
                        if isinstance(value, _Iterable):
                            if isinstance(value, (list, tuple, set)):
                                return [sanitize_for_json(v) for v in value]
                        # 其他类型统一转字符串
                        return str(value)
                    except Exception:
                        return None

                safe_meta = sanitize_for_json(meta_doc)
                with meta_path.open("w", encoding="utf-8") as f:
                    json.dump(safe_meta, f, ensure_ascii=False, indent=2)
                logging.info("已导出元数据到 %s", meta_path)
            except Exception as exc:  # pylint: disable=broad-except
                logging.error("写入元数据失败 %s: %s", meta_path, exc)

    logging.info("全部处理完成。")
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))

